5.2 Delegate – Methodenzeiger unter .NET
 
Das Prinzip der Delegate ist nicht ganz neu, wohl aber der Begriff an sich. »Delegate« ist das englische Wort für »delegieren« – etwas »weiterleiten«. Tatsächlich leitet ein Delegat weiter, er leitet nämlich einen Methodenaufruf an eine bestimmte Methode weiter.
5.2.1 Einführung in das Prinzip der Delegate
 
Die Technik, die sich dahinter verbirgt, wird in der Sprache C auch als Funktionszeiger bezeichnet. Zeigertechnik und .NET – das passt eigentlich nicht zusammen. Die Zeigertechnik – so interessant sie auch sein mag – birgt einige Nachteile in sich: Sie ist schwierig zu lernen, sie ist sehr komplex, die Programme sind zu kompliziert. Ein falscher Einsatz führt nicht selten zu Speicherzugriffsfehlern und damit zum Absturz eines laufenden Programms. Nicht umsonst haben die .NET-Entwickler (und auch die von Java) die Zeigertechnologie gemieden wie der Teufel das Weihwasser.
Dennoch gibt es Aufgabenstellungen, bei denen kein Weg an der Zeigertechnik vorbeiführt – auch nicht unter .NET, wenn auch in einer nicht sofort offensichtlichen Form. Wie Sie wissen, basiert ausnahmslos alles im .NET Framework auf Objekten. Da verwundert es nicht, dass auch die Methodenzeiger in ein Objekt verpackt und als Delegat bezeichnet ihren Weg in die Laufzeitumgebung finden.
| Ein Delegat ist ein Objekt, das den Zeiger auf eine Objektmethode beschreibt.
|
Bevor wir uns mit den Details von Delegaten beschäftigen, wollen wir uns zunächst an einem einfachen Beispiel die grundsätzliche Arbeitsweise verdeutlichen.
Die Operation, die von diesem Code ausgeführt wird, ist recht einfach: Der Anwender gibt zwei Zahlen an der Konsole ein und hat anschließend die Wahl, ob beide Zahlen addiert oder subtrahiert werden sollen. Das Resultat der Operation wird abhängig von der Wahl des Anwenders an der Konsole ausgegeben.
| // --------------------------------------------------------------
|
| // Beispiel: ...\Kapitel 5\EinfacherDelegat
|
| // --------------------------------------------------------------
|
| class Program {
|
| // Definition des Delegaten
|
| public delegate double ProcessOperation(double dblVar1, double dblVar2);
|
| static void Main(string[] args) {
|
| // Variable vom Typ des Delegaten
|
| ProcessOperation process;
|
| // Eingabe der Operanden
|
| Console.Write("Geben Sie den ersten Operanden ein: ");
|
| double input1 = Convert.ToDouble(Console.ReadLine());
|
| Console.Write("Geben Sie den zweiten Operanden ein: ");
|
| double input2 = Convert.ToDouble(Console.ReadLine());
|
| // Wahl der Operation
|
| Console.Write("Welche Operation wollen Sie ausführen?");
|
| Console.WriteLine("Addition – (A)");
|
| Console.WriteLine("Subtraktion – (S)");
|
| string wahl = Console.ReadLine().ToUpper();
|
| // in Abhängigkeit von der Wahl des Anwenders wird die
|
| // Variable 'process' mit einem Zeiger auf die
|
| // auszuführende Methode initialisiert
|
| if(wahl == "A")
|
| process = new ProcessOperation(Addition);
|
| else if(wahl == "S")
|
| process = new ProcessOperation(Subtraktion);
|
| else {
|
| Console.Write("Ungültige Eingabe");
|
| Console.ReadLine();
|
| return;
|
| }
|
| // Aufruf der Operation 'Addition' oder 'Subtraktion'
|
| // über den Delegaten
|
| double result = process(input1, input2);
|
| Console.WriteLine("----------------------------------");
|
| Console.Write("Ergebnis = {0}",result);
|
| Console.ReadLine();
|
| }
|
| public static double Addition(double x, double y) {
|
| return x + y;
|
| }
|
| public static double Subtraktion(double x, double y) {
|
| return x – y;
|
| }
|
| }
|
In der Klasse Class1 sind neben dem obligatorischen Einstiegspunkt in die Laufzeit zwei weitere statische Methoden definiert, die aus Main heraus aufgerufen werden und die beiden Operationen Addition und Subtraktion beschreiben.
Die Wahl, ob die beiden Zahlen addiert oder subtrahiert werden sollen, trifft der Anwender durch die Eingabe von »A« oder »S« an der Konsole. Um die Eingabe in Kleinschreibweise ebenfalls zu berücksichtigen, wird die Eingabe mit der Methode ToUpper der Klasse String in Großschreibweise umgewandelt.
| string wahl = Console.ReadLine().ToUpper();
|
Nachdem der Anwender seine Wahl getroffen hat, muss zuerst überprüft werden, wie diese ausgefallen ist, um entsprechend im Programmcode zu reagieren. Vermutlich hätten Sie eine solche Aufgabenstellung bisher wie folgt gelöst:
| double result;
|
| if(wahl == "A")
|
| result = Addition(input1, input2);
|
| else if(wahl == "S")
|
| result = Subtraktion(input1, input2);
|
Es gibt keinen Zweifel daran, dass diese Implementierung natürlich auch zum richtigen Ergebnis führt. Das Resultat der Operation wird in den Anweisungsblöcken hinter if bzw. else if abgerufen.
Nun betrachten wir die entscheidenden Anweisungen der Lösung im Beispiel EinfacherDelegate:
| if(wahl == "A")
|
| process = new ProcessOperation(Addition);
|
| else if(wahl == "S")
|
| process = new ProcessOperation(Subtraktion);
|
| ...
|
| double result = process(input1, input2);
|
Der Unterschied ist jetzt, dass wir das Ergebnis der Addition bzw. Subtraktion nun nicht mehr in den beiden Anweisungsblöcken der if-Struktur abrufen, sondern außerhalb derselben. Da aber außerhalb der if-Struktur die Wahl des Anwenders nicht bekannt sein kann, stehen wir vor der Frage, wie es möglich ist, eine Methode an einem Allgemeinplatz dynamisch aufrufen zu können. Die Antwort darauf ist prinzipiell nicht schwierig: Wir müssen einen Verweis auf die aufzurufende Methode in einer Variablen speichern, der später an einer beliebigen Stelle im Code ausgewertet werden kann.
Bisher kennen wir Verweise nur im Zusammenhang mit Objekten. Mit Objektverweisen werden zusammenhängende Datenblöcke im Hauptspeicher adressiert, in denen die Zustandsdaten eines ganz bestimmten Objekts beschrieben werden. Ein Verweis auf Programmcode ist im Grunde genommen nicht anders, zeigt aber auf Byte-Sequenzen, die anders interpretiert werden müssen – nämlich als ausführbarer Programmcode. Damit ist auch klar, dass ein Verweis auf Code anders definiert werden muss als der uns gebräuchliche Verweis auf Datenblöcke. Aus diesem Grund wurden in .NET die Delegaten eingeführt.
Wie schon oben erwähnt, kapselt ein Delegat den Zeiger auf eine Methode. Wir wissen zudem, dass .NET nur Objekte kennt. Sehen wir uns jetzt an, wie die Konstrukteure von C# diese beiden Anforderungen konzeptuell gelöst haben.
Im Code des Beispiels EinfacherDelegat wird mit
| public delegate double ProcessOperation(double dblVar1, double dblVar2);
|
ein Delegat definiert. Diese Definition erinnert ein wenig an die Methodensignatur einer Methode namens ProcessOperation, die zwei Parameter vom Typ double empfängt und als Rückgabewert einen double liefert – nur ergänzt um das Schlüsselwort delegate.
| Hinweis Ein Delegat kann sowohl innerhalb als auch außerhalb einer Klasse definiert werden. Ein in einer Klasse definierter Delegat ist immer an die Klasse gebunden.
|
Ein Delegat kapselt den Zeiger auf eine Methode, oder mit anderen Worten, er steht für einen beliebigen Methodenaufruf. Ganz beliebig ist der Methodenaufruf allerdings nicht, denn jede Methode hat eine exakt definierte Parameterliste mit Parametern eines bestimmten Typs. Ein Delegat beschreibt einen Zeiger auf eine Methode, wobei die Typen der Parameterliste der Methode, auf die der Delegat zeigt, mit der Parameterliste der delegate-Definition übereinstimmen muss.
In unserem Beispiel werden in der Parameterliste des Delegaten ProcessOperation zwei Parameter vom Typ double aufgeführt. Damit wäre dieser Delegat in der Lage, jede x-beliebige Methode eines x-beliebigen Objekts aufzurufen – vorausgesetzt, die Methode definiert eine Parameterliste, die genau zwei double-Argumente erwartet.
| Die Parameterliste einer Delegat-Definition entspricht der Parameterliste der Methode, auf die der Delegat zeigt.
|
Das ist nicht die einzige Bedingung, die an die Methode gestellt wird, die ein Delegat beschreibt. Der Rückgabewert spielt eine ebenso wichtige Rolle. Im Beispiel des Delegaten ProcessOperation muss die Methode in jedem Fall einen Rückgabewert vom Typ double haben. Weil sowohl Parameterliste als auch Rückgabetyp durch einen Delegaten eindeutig festgelegt werden, spricht man beim Konstrukt eines Delegaten auch von einem typisierten Funktionszeiger.
Nicht jede Methode hat einen Rückgabewert. Beabsichtigen Sie beispielsweise, einen Delegaten zu definieren, der in der Lage ist, einen Zeiger auf sämtliche Methoden zu beschreiben, die parameterlos sind und keinen Rückgabewert haben, sähe die Definition folgendermaßen aus:
| public delegate void MyDelegate();
|
Sie können die Definition eines Delegaten mit der Definition einer Klasse vergleichen, denn beide beschreiben einen Typ. Um ein konkretes Objekt zu erhalten, muss zuerst eine Variable vom Typ der Klasse deklariert werden – das ist bei einem Delegaten nicht anders. Im Beispiel dient dazu die Anweisung:
| ProcessOperation process;
|
Damit ist die Variable process vom Typ ProcessOperation deklariert, aber noch nicht initialisiert. Mit anderen Worten: process ist ein Delegat und kann auf eine Methode verweisen, die zwei double-Argumente erwartet und einen double als Resultat des Aufrufs zurückliefert. In diesem Moment weiß der Delegat allerdings noch nicht, um welche Methode es sich dabei genau handelt.
Die Initialisierung erfolgt – analog zur Instanziierung einer Klasse – mit dem Operator new unter Angabe des Delegatentyps. Dahinter gibt man in runden Klammern den Bezeichner der Methode an, die später vom Delegaten aufgerufen werden soll. In unserem Beispiel handelt es sich um
| process = new ProcessOperation(Addition);
|
und
| process = new ProcessOperation(Subtraktion);
|
Danach ist dem Delegaten bekannt, welche Methode ausgeführt werden soll: entweder Addition oder Subtraktion. Allerdings wird die Methode, auf die der Delegat in Form eines Zeigers verweist, noch nicht sofort gestartet, denn dazu bedarf es eines Anstoßes durch den Aufruf des Delegaten:
| double result = process(input1, input2);
|
Der Aufruf erinnert an den Aufruf einer Methode, dabei wird allerdings der Methodenname (hier: Addition bzw. Subtraktion) durch die Variable vom Typ des Delegaten ersetzt. In den Klammern werden die erforderlichen Argumente an die Methode übergeben.
5.2.2 Zusammenfassung der Arbeitsschritte
 
Um eine aufzurufende Methode erst zur Laufzeit festzulegen, ist das Konstrukt der Delegaten sicherlich sehr interessant. Allerdings ist die syntaktische Realisierung etwas gewöhnungsbedürftig, daher sollen hier die Schritte noch einmal allgemein zusammengefasst werden.
1.
Definieren Sie zuerst einen Delegaten, z.B.:
public delegate double ProcessOperation(double dblVar1, double dblVar2);
|
|
Als Modifizierer kommen private, protected, internal und public sowie der Modifizierer new in Frage. Letzterer wird in Kapitel 6 behandelt. |
|
|
|
| 2. |
Deklarieren Sie eine Variable vom Typ des Delegaten, z.B.: |
|
|
|
ProcessOperation process;
| 3. |
Erzeugen Sie ein Objekt vom Typ des Delegaten, und übergeben Sie dabei als Argument den Namen der Methode, die vom Delegaten aufgerufen werden soll, z.B.: |
|
|
|
process = new ProcessOperation(Addition);
| 4. |
Rufen Sie den Delegaten auf, und übergeben Sie dabei die Parameter, die von der Methode empfangen werden sollen, auf die der Delegat zeigt. z.B.: |
|
|
|
double result = process(input1, input2);
| Hinweis Wir haben noch nicht alle Gesichtspunkte erörtert, die im Zusammenhang mit den Delegaten von programmiertechnischem Interesse sind. Wenn wir uns jedoch weiter mit diesen Konstrukten beschäftigen wollen, sollten mehr Grundsätze der objektorientierten Programmierung bekannt sein. Daher wird das Thema an dieser Stelle unterbrochen und in Kapitel 7 noch einmal aufgegriffen.
|
5.2.3 Vereinfachter Aufruf eines Delegaten
 
Seit der Version 2.0 gibt es unter C# eine neue Notation, einen Delegaten zu instanzîieren und ihm gleichzeitig die auszuführende Methode anzugeben. Diese Neuerung ist etwas einfacher in der Handhabung und erspart uns ein wenig Tipparbeit. Sie können nämlich anstelle der Anweisung
| ProcessOperation process = new ProcessOperation(Addition);
|
auch wie folgt codieren:
| ProcessOperation process = Addition;
|
5.2.4 Anonyme Methoden
 
Delegate haben wir bisher in der Weise konstruiert, indem wir den Delegaten instanziiert und ihm als Parameter eine benannte Methode übergeben haben. Das setzt voraus, dass die Methode auch irgendwo im Programmcode namentlich definiert werden muss. Bei größeren Anwendungen könnte das dazu führen, dass sehr viele Methoden nur dazu dienen, einzig und allein einen bestimmten Delegaten zu bedienen. Der Code wird unübersichtlich.
Eine Ergänzung von C# 2.0 ist, dass der auszuführende Programmcode direkt mit dem Delegaten verknüpft werden kann. Der Code ist nicht mehr mit einem Methodenbezeichner namentlich verbunden und wird deshalb als anonyme Methode bezeichnet.
Wir wollen uns das an einem Beispiel ansehen. Des besseren Vergleichs wegen wird der Delegat zuerst mit einer benannten Methode verknüpft, anschließend wird derselbe Code als anonyme Methode implementiert.
| // Delegate-Deklaration
|
| public delegate long MyDelegate(int x, int y);
|
| class Program {
|
| static void Main(string[] args) {
|
| // Verknüpfung des Delegaten mit einer benannten Methode
|
| MyDelegate del = new MyDelegate(Program.Addition);
|
| Console.WriteLine("Ergebnis = {0}", del(13, 34));
|
| Console.ReadLine();
|
| }
|
| static long Addition(int value1, int value2) {
|
| Console.Write("Die Parameterwerte sind: ");
|
| Console.WriteLine("{0} und {1}", value1, value2);
|
| return value1 + value2;
|
| }
|
| }
|
Der Code sollte ohne Probleme verständlich sein, deshalb werden wir uns sofort der Variante mit der anonymen Methode zuwenden. Im Unterschied dazu wird der Code, der oben in der Methode Addition implementiert ist, jetzt direkt nach der Deklaration der Variablen vom Typ des Delegaten angegeben.
| // Delegate-Deklaration
|
| public delegate long MyDelegate(int x, int y);
|
| class Program {
|
| static void Main(string[] args) {
|
| MyDelegate del = delegate(int value1, int value2)
|
| // anonyme Methode
|
| {
|
| Console.Write("Die Parameterwerte sind: ");
|
| Console.WriteLine("{0} und {1}", value1, value2);
|
| return value1 + value2;
|
| };
|
| Console.WriteLine("Ergebnis = {0}", del(13, 34));
|
| Console.ReadLine();
|
| }
|
| }
|
Um einen Delegaten zu instanziieren und mit dem Rumpf einer anonymen Methode zu verbinden, dient ebenfalls das Schlüsselwort delegate, hinter dem eine Parameterliste entsprechend der Delegatendefinition angegeben ist. Handelt es sich um eine parameterlose, anonyme Methode, bleibt die Liste leer, und auf die Angabe der optionalen runden Klammern kann auch verzichtet werden.
Die allgemeine Syntax, mit der das Objekt eines Delegaten mit einer anonymen Methode verknüpft wird, lautet wie folgt:
| delegate [(Parameterliste)] {Anweisungsblock};
|
Da sich der Anweisungsblock einer anonymen Methode immer innerhalb einer »äußeren« Methode befindet, kann aus der anonymen Methode heraus auf jede andere Variable zugegriffen werden. Es gelten dabei die üblichen Regeln der Sichtbarkeit.
Anonyme Methoden unterliegen im Vergleich zu anderen Anweisungsblöcken nur einer Einschränkung: Mit den Sprunganweisungen continue, break und goto darf innerhalb einer anonymen Methode nicht zu einer Anweisung verzweigt werden, die außerhalb der anonymen Methode codiert ist. Ebenfalls unzulässig ist eine Sprunganweisung außerhalb einer anonymen Methode, deren Ziel innerhalb einer anonymen Methode zu finden ist. |